#
# iTunesDB.py
#
# Author: Duane Maxwell
# (c) Linspire Inc, 2005
#
#  This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#

import base64,struct,array

class FileWriteString:
	def __init__(self):
		self.index = 0
		self.s = array.array('c');
	
	def asString(self):
		return self.s.tostring()
		
	def toFile(self,f):
		self.s.tofile(f)
	
	def write(self,data):
		if self.index == len(self.s):
			self.s.fromstring(data)
		else:
			for i in xrange(len(data)):
				self.s[self.index+i] = data[i]
		self.index += len(data)

	def tell(self): return self.index
	def seek(self,i): self.index = i

class mh_atom:
	#
	# data object (mhod) types
	#
	TITLE = 1
	LOCATION = 2
	ALBUM = 3
	ARTIST = 4
	GENRE = 5
	FILETYPE = 6
	EQSETTING = 7
	COMMENT = 8
	CATEGORY = 9 # podcast category
	COMPOSER = 12
	GROUPING = 13
	DESCRIPTION = 14 # ie podcast show notes 
	ENCLOSUREURL = 15 # utf-8 string
	RSSURL = 16 # utf-8 string
	CHAPTER = 17 # subsongs within mhit
	SUBTITLE = 18
	SMARTPLAYLIST_PREF = 50
	SMARTPLAYLIST_DATA = 51
	LIBRARYPLAYLIST_INDEX = 52
	PLAYLIST_SETTINGS = 100

	#
	# known database versions
	#
	VERSION4_2 = 9
	VERSION4_5 = 10
	VERSION4_7 = 11
	VERSION4_8 = 12
	VERSION4_9 = 13
	VERSION5_0 = 14
	
	#
	# playlist sort orders
	#
	SORT_ORDER_MANUAL = 1
	SORT_ORDER_TITLE = 3
	SORT_ORDER_ALBUM = 4
	SORT_ORDER_ARTIST = 5
	SORT_ORDER_BITRATE = 6
	SORT_ORDER_GENRE = 7
	SORT_ORDER_KIND = 8
	SORT_ORDER_DATEMODIFIED = 9
	SORT_ORDER_TRACKNUM = 10
	SORT_ORDER_SIZE = 11
	SORT_ORDER_DURATION = 12
	SORT_ORDER_YEAR = 13
	SORT_ORDER_SAMPLERATE = 14
	SORT_ORDER_COMMENT = 15
	SORT_ORDER_DATEADDED = 16
	SORT_ORDER_EQUALIZER = 17
	SORT_ORDER_COMPOSER = 18
	SORT_ORDER_PLAYCOUNT = 20
	SORT_ORDER_LASTPLAYED = 21
	SORT_ORDER_DISCNUM = 22
	SORT_ORDER_RATING = 23
	SORT_ORDER_RELEASEDATE = 24
	SORT_ORDER_BPM = 25
	SORT_ORDER_GROUPING = 26
	SORT_ORDER_CATEGORY = 27
	SORT_ORDER_DESCRIPTION = 28
	
	def __init__(self,version=VERSION4_2):
		self._version = version

	#def __repr__(self):
	#	return str(vars(self))

	def getc(self,f): return f.read(1)	
	def putc(self,f,c): f.write(c)
	def getb(self,f): return ord(f.read(1))
	def putb(self,f,b): f.write(chr(b))
	def getuw(self,f): return self.getb(f)+(self.getb(f)<<8)
	
	def putuw(self,f,w):
		self.putb(f,w & 0xff)
		self.putb(f,w>>8)
	
	def getul(self,f): return self.getuw(f) + (long(self.getuw(f))<<16)
	
	def putul(self,f,l):
		self.putuw(f,l & 0xffff)
		self.putuw(f,l >> 16)
	
	def get4cc(self,f): return f.read(4)
	def put4cc(self,f,cc): f.write(cc)
	
	def gets(self,f,len):
		s = f.read(len)
		return unicode(s,"utf-16","x")
	
	def puts(self,f,s):
		s = s.encode('utf-16')
		f.write(s[2:]) # avoid writing BOM
	
	def getas(self,f,len):
		return f.read(len)
	
	def putas(self,f,s):
		s = s.encode('utf-8')
		f.write(s)
	
	def getf(self,f):
		return struct.unpack('f',f.read(4))[0]
	
	def putf(self,f,val):
		f.write(struct.pack('f',val))

	def tell(self,f): return f.tell()
	def seek(self,f,pos): f.seek(pos)
	def skip(self,f,count): self.seek(f,self.tell(f)+count)
	
	def backpatch(self,f,mark):
		here = self.tell(f)
		self.seek(f,mark+8)
		self.putul(f,here-mark)
		self.seek(f,here)

	def atom(self,f,version = VERSION4_2): return self.parseAtom(self.get4cc(f),f,version)
	
	def atomForTag(self,tag,version=VERSION4_2):
		atom = None
		if tag=='mhbd': atom = mhbd_atom(version)
		if tag=='mhsd': atom = mhsd_atom(version)
		if tag=='mhlt': atom = mhlt_atom(version)
		if tag=='mhit': atom = mhit_atom(version)
		if tag=='mhod': atom = mhod_atom(version)
		if tag=='mhlp': atom = mhlp_atom(version)
		if tag=='mhyp': atom = mhyp_atom(version)
		if tag=='mhip': atom = mhip_atom(version)
		return atom

	def parseAtom(self,tag,f,version=VERSION4_2):
		#print "%010x parsing %s" % (f.tell(),tag)
		atom = self.atomForTag(tag,version)
		if atom:
			atom = atom.read(f,version)
			return atom
		else:
			print "%s: unknown atom %s (%s)" % (self.className(),tag,' '.join(map(lambda c: "%02x" % ord(c),tag)))
			return None
	
	def simpleHeader(self,f):
		t = self.tell(f)
		header1 = self.getul(f)+t-4
		header2 = self.getul(f)+t-4
		return (header1,header2)
	
	def listHeader(self,f):
		t = self.tell(f)
		return self.getul(f)+t-4
	
	def putSimpleHeader(self,f,tag,size):
		#print "writing",tag
		mark = self.tell(f)
		self.put4cc(f,tag)
		self.putul(f,size)
		self.putul(f,0)
		return mark
	
	def putListHeader(self,f,tag,size):
		mark = self.tell(f)
		self.put4cc(f,tag)
		self.putul(f,size)
		return mark
	
	def putZeros(self,f,count):
		for i in xrange(count):
			self.putul(f,0)
	
	def pad(self,f,mark,size):
		delta = size-(self.tell(f)-mark)
		if delta>=0:
			while delta>=4:
				self.putul(f,0)
				delta = delta-4
			while delta:
				self.putb(f,0)
				delta = delta-1
		else:
			print "%s: header too large" % (self.className())
	
	def validateHeader(self,f,mark,size):
		calcSize = self.tell(f)-mark
		if calcSize!=size:
			print "%s: broken header - expected %d, got %d" % (self.className(),size,calcSize)
		
	def read(self,f,version= VERSION4_2): print "%s - must override read()" % (self.className())
	def write(self,f,version= VERSION4_2): print "%s - must override write()" % (self.className())
	
	def className(self):
		return self.__class__.__name__
	
	def getPList(self):
		return {'version':self._version}
	
	def setPList(self,p):
		self._version = p['version']
		return self

#
# db = DataBase atom
#		
class mhbd_atom(mh_atom):
	def __init__(self,version = mh_atom.VERSION4_2):
		mh_atom.__init__(self,version)
		self.versionhi = 1
		self.versionlo = version
		self.res1 = 0
		self.res2 = 0
		self.res3 = 2
		self.makeChildren(version)
	
	def makeChildren(self,version):
		self.children = []
		self.mhsd_songs = mhsd_atom(version,1)
		self.children.append(self.mhsd_songs)
		self.mhsd_playlists = mhsd_atom(version,2)
		self.children.append(self.mhsd_playlists)
		if version>=mh_atom.VERSION4_9:
			self.mhsd_podcasts = mhsd_atom(version,3)
			self.children.append(self.mhsd_podcasts)

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.versionhi = self.getul(f) # should be 1
		self.versionlo = self.getul(f)
		self._version = self.versionlo
		count = self.getul(f) # should be 2 or 3
#		if self.versionlo<mh_atom.VERSION4_9 and self.count!=2:
#			print "%s - expected 2 children, got %d" % (self.className(),self.count)
		self.res1 = self.getul(f)
		self.res2 = self.getul(f)
		self.res3 = self.getul(f) # should be 2
		self.seek(f,h1)
		self.children = []
		for i in xrange(count):
			child = self.atom(f,self.versionlo)
			self.children.append(child)
			if child.index==1: self.mhsd_songs = child
			elif child.index==3: self.mhsd_playlists = child
			elif child.index==2: self.mhsd_podcasts = child
			else:
				print "mhbd_atom.read unknown child index!"
		if count==2:
			self.mhsd_playlists,self.mhsd_podcasts = self.mhsd_podcasts,self.mhsd_playlists
		return self
	
	def write(self,f,version):
		#print "iTunesDB.mhbd_atom: preparing database for write"
		self.prepareToWrite()
		#print "iTunesDB:mhbd_atom: writing database"
		mark = self.putSimpleHeader(f,'mhbd',0x68)
		#f.versionlo = self.versionlo
		self.putul(f,self.versionhi)
		self.putul(f,self.versionlo)
		self.putul(f,len(self.children))
		self.putul(f,self.res1)
		self.putul(f,self.res2)
		self.putul(f,self.res3)
		self.pad(f,mark,0x68)
		self.validateHeader(f,mark,0x68)
		for child in self.children:
			child.write(f,version)
		self.backpatch(f,mark)
		#print "iTunesDB:mhbd_atom: done"
	
	def prepareToWrite(self):
		self.playlists().prepareToWrite(self.songs())

	def songs(self): return self.mhsd_songs
	def playlists(self): return self.mhsd_playlists
	def podcasts(self): return self.mhsd_podcasts
	
	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhbd'
		p['versionlo'] = self.versionlo
		p['versionhi'] = self.versionhi
		p['res1'] = self.res1
		p['res2'] = self.res2
		p['children'] = [child.getPList() for child in self.children]
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.versionlo = p['versionlo']
		self.versionhi = p['versionhi']
		self.res1 = p['res1']
		self.res2 = p['res2']
		self.children = []
		for item in p['children']:
			atom = self.atomForTag(item['tag'],self.versionlo)
			if atom:
				atom = atom.setPList(item)
				self.children.append(atom)
		return self

#
# ds = DataStore item
#
class mhsd_atom(mh_atom):
	def __init__(self,version,index = 1):
		mh_atom.__init__(self,version)
		self.index = index
		self.makeChildForIndex(version)
	
	def makeChildForIndex(self,version):
		if self.index==1: self.child = mhlt_atom(version)
		if self.index==2: self.child = mhlp_atom(version)
		if self.index==3: self.child = mhlp_atom(version)

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.index = self.getul(f)
		#print "mhsd: type",self.index
		self.seek(f,h1)
		tag = self.get4cc(f)
		if self.index==1 and tag!='mhlt':
			print "mhsd_atom.read(): expected mhlt (index 1)"
			return
		elif self.index==2 and tag!='mhlp':
			print "mhsd_atom.read(): expected mhlp (index 2)"
			return
		elif self.index==3 and tag!='mhlp':
			print "mhsd_atom.read(): expected mhlp (index 3)"
		self.makeChildForIndex(version)
		self.child = self.child.read(f,version)
		return self
	
	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhsd',0x60)
		self.putul(f,self.index)
		self.pad(f,mark,0x60)
		self.validateHeader(f,mark,0x60)
		self.child.write(f,version)
		self.backpatch(f,mark)
	
	def songItems(self):
		if self.index==1: return self.child
		else: return None
	
	def playlistItems(self):
		if self.index in [2,3]: return self.child
		else: return None
	
	def podcastItems(self):
		if self.index in [2,3]: return self.child
		else: return None

	def prepareToWrite(self,songs):
		self.playlistItems().prepareToWrite(songs.songItems())
	
	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhsd'
		p['index'] = self.index
		p['child'] = self.child.getPList()
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.index = p['index']
		child = p['child']
		atom = self.atomForTag(child['tag'],self._version)
		if atom:
			atom = atom.setPList(child)
			self.child = atom
		return self

#
# tl = TrackList atom
#
class mhlt_atom(mh_atom):
	def __init__(self,version):
		mh_atom.__init__(self,version)
		self.children = []
		
	def read(self,f,version):
		h1 = self.listHeader(f)
		count = self.getul(f)
		self.seek(f,h1)
		for i in xrange(count):
			self.children.append(self.atom(f,version))
		return self
	
	def write(self,f,version):
		mark = self.putListHeader(f,'mhlt',0x5c)
		self.putul(f,len(self.children))
		self.pad(f,mark,0x5c)
		self.validateHeader(f,mark,0x5c)
		for child in self.children:
			child.write(f,version)
		#self.backpatch(f,mark)
	
	def count(self): return len(self.children)
	
	def addTrack(self,id=0):
		if id==0:
			id = 1000
			found = True
			while found:
				found = False
				for child in self.children:
					if child.trackID>=id:
						id = child.trackID+2
						found = True
		track = mhit_atom(self._version)
		track.trackID = id
		self.children.append(track)
		#print "added track",track.trackID
		return track
	
	def deleteTrack(self,index):
		try: del self.children[index]
		except: pass
	
	def deleteTrackByID(self,id):
		try:  self.children.remove(self.trackByID(id))
		except: pass
	
	def track(self,index):
		try: return self.children[index]
		except: return None
	
	def trackByID(self,id):
		for child in self.children:
			if child.trackID==id:
				return child
		return None
	
	def indexOfTrackID(self,id):
		index = 0
		for child in self.children:
			if child.trackID==id: return index
			index = index+1
		return None

	def clearTracks(self):
		self.children = []
	
	def tracks(self):
		return self.children

	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhlt'
		p['children'] = [child.getPList() for child in self.children]
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.children = []
		for item in p['children']:
			atom = self.atomForTag(item['tag'],self._version)
			if atom:
				atom = atom.setPList(item)
				self.children.append(atom)
		return self

#
# ti = TrackItem atom
#
class mhit_atom(mh_atom):
	def __init__(self,version):
		mh_atom.__init__(self,version)
		self.trackID = 0
		self.visible = 1
		self.filetype = 0
		self.vbr = 0
		self.compilation = 0
		self.type = 0
		self.rating = 0
		self.lastPlayedTime = 0
		self.size = 0
		self.duration = 0
		self.trackNumber = 0
		self.trackCount = 0
		self.year = 0
		self.bitRate = 0
		self.unk8 = 0
		self.sampleRate = 0
		self.volume = 0
		self.startTime = 0
		self.endTime = 0
		self.soundCheck = 0
		self.playCount = 0
		self.playCount2 = 0
		self.addedTime = 0
		self.discNumber = 0
		self.discCount = 0
		self.userid = 0
		self.lastModificationTime = 0
		self.bookMarkTime = 0
		self.dbidlo = 0
		self.dbidhi = 0
		self.checked = 0
		self.appRating = 0
		self.BPM = 0
		self.artworkCount = 0
		self.unk9 = 0xffff
		self.artworkSize = 0
		self.unk11 = 0
		self.sampleRate2 = 0
		self.unk13= 0
		self.unk14 = 0x0000000c
		self.unk15 = 0
		self.unk16 = 0
		self.unk17 = 0
		self.unk18 = 0
		self.unk19 = 2
		self.dbid2lo = 0
		self.dbid2hi = 0
		self.unk20 = 0
		self.unk21 = 0
		self.unk22 = 0
		self.unk23 = 0
		self.unk24 = 0
		self.unk25 = 0
		self.unk26 = 0
		self.unk27 = 0
		self.unk28 = 0
		self.unk29 = 0
		self.unk30 = 0
		self.unk31 = 0
		self.unk32 = 0
		self.unk33 = 0
		self.unk34 = 0
		self.unk35 = 0
		self.unk36 = 0
		self.children = []

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		count = self.getul(f)
		self.trackID = self.getul(f)
		self.visible = self.getul(f)
		self.filetype = self.getul(f) # 0 on HD-based iPods, but "MP3 " for shuffle
		self.vbr = self.getuw(f) # CBR MP3=0x100, VBR MP3=0x101, AAC=0x0
		self.compilation = self.getb(f)
		#self.type = self.getb(f)
		self.rating = self.getb(f) # rating*20
		self.lastPlayedTime = self.getul(f)
		self.size = self.getul(f)
		self.duration = self.getul(f) # in milliseconds
		self.trackNumber= self.getul(f)
		self.trackCount= self.getul(f)
		self.year = self.getul(f)
		self.bitRate = self.getul(f)
		self.unk8 = self.getuw(f)
		self.sampleRate = self.getuw(f)
		self.volume = self.getul(f) # -255 to +255
		self.startTime = self.getul(f) # in milliseconds
		self.endTime = self.getul(f) # in milliseconds
		self.soundCheck = self.getul(f) # = 1000*10^(-0.1*Y) whe Y is adjustment in dB
		self.playCount = self.getul(f)
		self.playCount2 = self.getul(f) # should be same as self.playCount
		self.addedTime = self.getul(f)
		self.discNumber = self.getul(f)
		self.discCount = self.getul(f)
		self.userid = self.getul(f) # AppleStore user ID, 0 otherwise (used for DRM)
		self.lastModificationTime = self.getul(f)
		self.bookMarkTime = self.getul(f)
		self.dbidlo = self.getul(f) # unique ID across databases (used to match against artwork db)
		self.dbidhi = self.getul(f)
		self.checked = self.getb(f)
		self.appRating = self.getb(f)
		self.BPM = self.getuw(f)
		self.artworkCount = self.getuw(f)
		self.unk9 = self.getuw(f) # 0xffff for AAC and MP3, 0x0 for WAV, 0x1 for Audible
		self.artworkSize = self.getul(f)
		self.unk11 = self.getul(f)
		self.sampleRate2 = self.getf(f) # same as sampleRate but in iEEE floating point
		self.unk13= self.getul(f) # some sort of timestamp (music store)?
		self.unk14 = self.getul(f) # 0x0000000c or 0x0100000c for MP3, 0x10000033 for AAC, 0x01000029 for Audible, 0 for WAV
		self.unk15 = self.getul(f) # used for music store?
		self.unk16 = self.getul(f)
		if version>=mh_atom.VERSION4_8:
			self.unk17 = self.getul(f)
			self.unk18 = self.getul(f)
			self.unk19 = self.getul(f)
			self.dbid2lo = self.getul(f)
			self.dbid2hi = self.getul(f)
			self.unk20 = self.getul(f)
			self.unk21 = self.getul(f)
			self.unk22 = self.getul(f)
			self.unk23 = self.getul(f)
			self.unk24 = self.getul(f)
			self.unk25 = self.getul(f)
			self.unk26 = self.getul(f)
			self.unk27 = self.getul(f)
			self.unk28 = self.getul(f)
			self.unk29 = self.getul(f)
			self.unk30 = self.getul(f)
			self.unk31 = self.getul(f)
			self.unk32 = self.getul(f)
			self.unk33 = self.getul(f)
			self.unk34 = self.getul(f)
			self.unk35 = self.getul(f)
			self.unk36 = self.getul(f)
		self.seek(f,h1)
		for i in xrange(count):
			self.children.append(self.atom(f,version))
		self.seek(f,h2)
		return self
	
	def write(self,f,version):
		headerSize = 0x9c
		if version>=mh_atom.VERSION4_8: headerSize = 0xf4
		mark = self.putSimpleHeader(f,'mhit',headerSize)
		self.putul(f,len(self.children))
		self.putul(f,self.trackID)
		self.putul(f,self.visible)
		self.putul(f,self.filetype)
		self.putuw(f,self.vbr)
		self.putb(f,self.compilation)
		#self.putb(f,self.type)
		self.putb(f,self.rating)
		self.putul(f,self.lastPlayedTime)
		self.putul(f,self.size)
		self.putul(f,self.duration)
		self.putul(f,self.trackNumber)
		self.putul(f,self.trackCount)
		self.putul(f,self.year)
		self.putul(f,self.bitRate)
		self.putuw(f,self.unk8)
		self.putuw(f,self.sampleRate)
		self.putul(f,self.volume)
		self.putul(f,self.startTime)
		self.putul(f,self.endTime)
		self.putul(f,self.soundCheck)
		self.putul(f,self.playCount)
		self.putul(f,self.playCount2)
		self.putul(f,self.addedTime)
		self.putul(f,self.discNumber)
		self.putul(f,self.discCount)
		self.putul(f,self.userid)
		self.putul(f,self.lastModificationTime)
		self.putul(f,self.bookMarkTime)
		self.putul(f,self.dbidlo)
		self.putul(f,self.dbidhi)
		self.putb(f,self.checked)
		self.putb(f,self.appRating)
		self.putuw(f,self.BPM)
		self.putuw(f,self.artworkCount)
		self.putuw(f,self.unk9)
		self.putul(f, self.artworkSize)
		self.putul(f,self.unk11)
		self.putf(f,float(self.sampleRate2))
		self.putul(f,self.unk13)
		self.putul(f,self.unk14)
		self.putul(f,self.unk15)
		self.putul(f,self.unk16)
		if version>=mh_atom.VERSION4_8:
			self.putul(f,self.unk17)
			self.putul(f,self.unk18)
			self.putul(f,self.unk19)
			self.putul(f,self.dbid2lo)
			self.putul(f,self.dbid2hi)
			self.putul(f,self.unk20)
			self.putul(f,self.unk21)
			self.putul(f,self.unk22)
			self.putul(f,self.unk23)
			self.putul(f,self.unk24)
			self.putul(f,self.unk25)
			self.putul(f,self.unk26)
			self.putul(f,self.unk27)
			self.putul(f,self.unk28)
			self.putul(f,self.unk29)
			self.putul(f,self.unk30)
			self.putul(f,self.unk31)
			self.putul(f,self.unk32)
			self.putul(f,self.unk33)
			self.putul(f,self.unk34)
			self.putul(f,self.unk35)
			self.putul(f,self.unk36)
		self.pad(f,mark,headerSize)
		self.validateHeader(f,mark,headerSize)
		for child in self.children:
			child.write(f,version)
		self.backpatch(f,mark)
	
	def addStringOfType(self,type = 0):
		s = self.findStringOfType(type)
		if s: return s
		s = mhod_atom_String(self._version)
		s.type = type
		self.children.append(s)
		return s
	
	def findStringOfType(self,type=0):
		for child in self.children:
			if child.type==type:
				return child
		return None
	
	def deleteStringOfType(self,type=0):
		try: self.children.remove(self.findChildOfType(type))
		except: pass
	
	def dumpStrings(self):
		for child in self.children:
			if child.type<50:
				try: print child.type,child.string
				except: pass
	
	def stringOfType(self,type=0):
		child = self.findStringOfType(type)
		if child: return child.string
		return None

	def title(self): return self.stringOfType(1)
	def location(self): return self.stringOfType(2)
	def album(self): return self.stringOfType(3)
	def artist(self): return self.stringOfType(4)
	def genre(self): return self.stringOfType(5)
	def composer(self): return self.stringOfType(12)

	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhit'
		p['trackID'] = self.trackID
		p['visible'] = self.visible
		p['filetype'] = self.filetype
		p['vbr'] = self.vbr
		p['compilation'] = self.compilation
		p['rating'] = self.rating 
		p['lastPlayedTime'] = self.lastPlayedTime
		p['size'] = self.size
		p['duration'] = self.duration
		p['trackNumber'] = self.trackNumber
		p['trackCount'] = self.trackCount
		p['year'] = self.year
		p['bitRate'] = self.bitRate
		p['unk8'] = self.unk8
		p['sampleRate'] = self.sampleRate
		p['volume'] = self.volume
		p['startTime'] = self.startTime 
		p['endTime'] = self.endTime
		p['soundCheck'] = self.soundCheck
		p['playCount'] = self.playCount 
		p['playCount2']  = self.playCount2
		p['addedTime'] = self.addedTime
		p['discNumber'] = self.discNumber
		p['discCount'] = self.discCount
		p['userid'] = self.userid
		p['lastModificationTime'] = self.lastModificationTime 
		p['bookMarkTime'] = self.bookMarkTime
		p['dbidlo'] = self.dbidlo
		p['dbidhi'] = self.dbidhi
		p['checked'] = self.checked
		p['appRating'] = self.appRating
		p['BPM'] = self.BPM
		p['artworkCount'] = self.artworkCount
		p['unk9'] = self.unk9
		p['artworkSize'] = self.artworkSize
		p['unk11'] = self.unk11
		p['sampleRate2'] = float(self.sampleRate2)
		p['unk13'] = self.unk13
		p['unk14'] = self.unk14
		p['unk15'] = self.unk15 
		p['unk16'] = self.unk16
		if self._version>=mh_atom.VERSION4_8:
			p['unk17'] = self.unk17
			p['unk18'] = self.unk18
			p['unk19'] = self.unk19
			p['dbid2lo'] = self.dbid2lo
			p['dbid2hi'] = self.dbid2hi
			p['unk20'] = self.unk20
			p['unk21'] = self.unk21
			p['unk22'] = self.unk22
			p['unk23'] = self.unk23
			p['unk24'] = self.unk24
			p['unk25'] = self.unk25
			p['unk26'] = self.unk26
			p['unk27'] = self.unk27
			p['unk28'] = self.unk28
			p['unk29'] = self.unk29
			p['unk30'] = self.unk30
			p['unk31'] = self.unk31
			p['unk32'] = self.unk32
			p['unk33'] = self.unk33
			p['unk34'] = self.unk34
			p['unk35'] = self.unk35
			p['unk36'] = self.unk36
		p['children'] = [child.getPList() for child in self.children]
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.trackID = p['trackID']
		self.visible = p['visible']
		self.filetype = p['filetype']
		self.vbr = p['vbr']
		self.compilation = p['compilation']
		self.rating = p['rating']
		self.lastPlayedTime = p['lastPlayedTime']
		self.size = p['size']
		self.duration = p['duration']
		self.trackNumber= p['trackNumber']
		self.trackCount= p['trackCount']
		self.year = p['year']
		self.bitRate = p['bitRate']
		self.unk8 = p['unk8']
		self.sampleRate = float(p['sampleRate'])
		self.volume = p['volume']
		self.startTime = p['startTime']
		self.endTime = p['endTime']
		self.soundCheck = p['soundCheck']
		self.playCount = p['playCount']
		self.playCount2 = p['playCount2']
		self.addedTime = p['addedTime']
		self.discNumber = p['discNumber']
		self.discCount = p['discCount']
		self.userid = p['userid']
		self.lastModificationTime = p['lastModificationTime']
		self.bookMarkTime = p['bookMarkTime']
		self.dbidlo = p['dbidlo']
		self.dbidhi = p['dbidhi']
		self.checked = p['checked']
		self.appRating = p['appRating']
		self.BPM = p['BPM']
		self.artworkCount = p['artworkCount']
		self.unk9 = p['unk9']
		self.artworkSize = p['artworkSize']
		self.unk11 = p['unk11']
		self.sampleRate2 = p['sampleRate2']
		self.unk13 = p['unk13']
		self.unk14 = p['unk14']
		self.unk15 = p['unk15']
		self.unk16 = p['unk16']
		if self._version>=mh_atom.VERSION4_8:
			self.unk17 = p['unk17']
			self.unk18 = p['unk18']
			self.unk19 = p['unk19']
			self.dbid2lo = p['dbid2lo']
			self.dbid2hi = p['dbid2hi']
			self.unk20 = p['unk20']
			self.unk21 = p['unk21']
			self.unk22 = p['unk22']
			self.unk23 = p['unk23']
			self.unk24 = p['unk24']
			self.unk25 = p['unk25']
			self.unk26 = p['unk26']
			self.unk27 = p['unk27']
			self.unk28 = p['unk28']
			self.unk29 = p['unk29']
			self.unk30 = p['unk30']
			self.unk31 = p['unk31']
			self.unk32 = p['unk32']
			self.unk33 = p['unk33']
			self.unk34 = p['unk34']
			self.unk35 = p['unk35']
			self.unk36 = p['unk36']
		self.children = []
		for item in p['children']:
			atom = self.atomForTag(item['tag'],self._version)
			if atom:
				atom = atom.setPList(item)
				self.children.append(atom)
		return self

#
# do = DataObject atom
#
class mhod_atom(mh_atom): # generic, subclassed on read
	def __init__(self,version):
		mh_atom.__init__(self,version)
	
	def atomForType(self,type,version):
		if type==15 or type==16:
			atom = mhod_atom_URL(version)
		elif type==17:
			atom = mhod_atom_Raw(version)
		elif type<50:
			atom = mhod_atom_String(version)
		elif type==52:
			atom = mhod_atom_LibraryIndex(version)
		else:
			atom = mhod_atom_Raw(version)
		return atom

	def read(self,f,version):
		mark = self.tell(f)
		(h1,h2) = self.simpleHeader(f)
		type = self.getul(f)
		self.seek(f,mark)
		atom = self.atomForType(type,version)
		return atom.read(f,version)

	def write(self,f,version):
		print "mhod_atom.write() - Must override!!"
		sys.exit(0)
	
	def getPList(self):
		p =  mh_atom.getPList(self)
		p['tag'] = 'mhod'
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		type = p['type']
		atom = self.atomForType(type,self._version)
		return atom.setPList(p)
		
	def prepareToWrite(self,songs):
		pass

class mhod_atom_String(mhod_atom):
	def __init__(self,version):
		mhod_atom.__init__(self,version)
		self.type = 0
		self.res1 = 0
		self.res2 = 0
		self.position = 1
		self.res3 = 0
		self.res4 = 0

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.type = self.getul(f)
		self.res1 = self.getul(f)
		self.res2 = self.getul(f)
		self.position = self.getul(f)
		length = self.getul(f)
		self.res3 = self.getul(f)
		self.res4 = self.getul(f)
		self.string = self.gets(f,length)
		self.seek(f,h2)
		return self

	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhod',0x18)
		self.putul(f,self.type)
		self.putul(f,self.res1)
		self.putul(f,self.res2)
		self.putul(f,self.position)
		self.putul(f,len(self.string)*2)
		self.putul(f,self.res3)
		self.putul(f,self.res4)
		self.puts(f,self.string)
		self.backpatch(f,mark)

	def setString(self,s):
		self.string = s
	
	def getPList(self):
		p = mhod_atom.getPList(self)
		p['type'] = self.type
		p['res1'] = self.res1
		p['res2'] = self.res2
		p['position'] = self.position
		p['res3'] = self.res3
		p['res4'] = self.res4
		p['string'] = self.string
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.type = p['type']
		self.res1 = p['res1']
		self.res2 = p['res2']
		self.position = p['position']
		self.res3 = p['res3']
		self.res4 = p['res4']
		self.string = p['string']
		return self

class mhod_atom_URL(mhod_atom):
	def __init__(self,version,type=15):
		mhod_atom.__init__(self,version)
		self.type = type
		self.res1 = 0
		self.res2 = 0

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.type = self.getul(f)
		self.res1 = self.getul(f)
		self.res2 = self.getul(f)
		length = h2-f.tell()
		self.string = self.getas(f,length)
		self.seek(f,h2)
		return self

	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhod',0x18)
		self.putul(f,self.type)
		self.putul(f,self.res1)
		self.putul(f,self.res2)
		self.putas(f,self.string)
		self.backpatch(f,mark)

	def setString(self,s):
		self.string = s

	def getPList(self):
		p = mhod_atom.getPList(self)
		p['type'] = self.type
		p['res1'] = self.res1
		p['res2'] = self.res2
		p['string'] = self.string
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.type = p['type']
		self.res1 = p['res1']
		self.res2 = p['res2']
		self.string = p['string']
		return self

class mhod_atom_Raw(mhod_atom):
	def __init__(self,version,type=17):
		mhod_atom.__init__(self,version)
		self.type = type
		self.res1 = 0
		self.res2 = 0
	
	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.type = self.getul(f)
		self.res1 = self.getul(f)
		self.res2 = self.getul(f)
		length = h2-h1
		self.rawData = f.read(length)
		self.seek(f,h2)
		return self

	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhod',0x18)
		self.putul(f,self.type)
		self.putul(f,self.res1)
		self.putul(f,self.res2)
		f.write(self.rawData)
		self.backpatch(f,mark)

	def getPList(self):
		p = mhod_atom.getPList(self)
		p['type'] = self.type
		p['res1'] = self.res1
		p['res2'] = self.res2
		p['rawData'] = base64.encodestring(self.rawData)
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.type = p['type']
		self.res1 = p['res1']
		self.res2 = p['res2']
		self.rawData = base64.decodestring(p['rawData'])
		return self

class mhod_atom_LibraryIndex(mhod_atom):
	def __init__(self,version,index = 3):
		mhod_atom.__init__(self,version)
		self.type = 52
		self.res1 = 0
		self.res2 = 0
		self.sortIndex = index
		self.entries = []
	
	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.type = self.getul(f)
		self.res1 = self.getul(f)
		self.res2 = self.getul(f)
		self.sortIndex = self.getul(f) # 3==title, 4==album, 5==artist, 7=genre, 18=composer
		count = self.getul(f) # how many indices
		f.read(10*4) # padding
		self.entries = []
		for i in xrange(count):
			self.entries.append(self.getul(f))
		self.seek(f,h2)
		return self

	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhod',0x18)
		self.putul(f,self.type)
		self.putul(f,self.res1)
		self.putul(f,self.res2)
		self.putul(f,self.sortIndex)
		self.putul(f,len(self.entries))
		self.putZeros(f,10)
		for i in self.entries:
			self.putul(f,i)
		self.backpatch(f,mark)

	#
	# build the sorted lists of tracks to optimize the UI on the iPod
	#
	def prepareToWrite(self,songs):
		#print "mhod.prepareToWrite: type",self.sortIndex
		#print "before:",self.entries
		d = {}
		index = 0
		for song in songs.tracks():
			if self.sortIndex == 3: key = song.title()
			elif self.sortIndex==4: key = song.album()
			elif self.sortIndex==5: key = song.artist()
			elif self.sortIndex==7: key = song.genre()
			elif self.sortIndex==18: key = song.composer()
			if key==None: key = u"\xffff" # push unknowns to the end
			d.setdefault(key,[]).append(long(index))
			index = index+1
		keys = d.keys()
		keys.sort()
		self.entries = []
		for key in keys:
			self.entries.extend(d[key])
		#print "after:",self.entries

	def getPList(self):
		p = mhod_atom.getPList(self)
		p['type'] = self.type
		p['res1'] = self.res1
		p['res2'] = self.res2
		p['sortIndex'] = self.sortIndex
		p['entries'] = self.entries[:]
		return p
	
	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.type = p['type']
		self.res1 = p['res1']
		self.res2 = p['res2']
		self.sortIndex = p['sortIndex']
		self.entries = p['entries'][:]
		return self

#
# pl = PlaylistList item
#
class mhlp_atom(mh_atom):
	def __init__(self,version):
		mh_atom.__init__(self,version)
		self.children = []
		self.buildDefaultChildren(version)
	
	def buildDefaultChildren(self,version):
		self.children.append(mhyp_atom(version,1))
	
	def read(self,f,version):
		h1 = self.listHeader(f)
		count = self.getul(f)
		#print "mhlp: got %d atoms" % count
		self.seek(f,h1)
		self.children = []
		for i in xrange(count):
			self.children.append(self.atom(f,version))
		return self
	
	def write(self,f,version):
		mark = self.putListHeader(f,'mhlp',0x5c)
		self.putul(f,len(self.children))
		self.pad(f,mark,0x5c)
		for child in self.children:
			child.write(f,version)
		#self.backpatch(f,mark)
	
	def count(self):
		return len(self.children)

	def playlist(self,index):
		try: return self.children[index]
		except: return None

	def uniquePlaylistID(self):
		id = 0
		for child in self.children:
			if id<=child.playlistIDLo:
				id = child.playlistIDLo+2
		return id

	def addPlaylist(self,playlist = None):
		if playlist==None:
			id = self.uniquePlaylistID()
			playlist = mhyp_atom(self._version)
			playlist.playlistIDLo = id
		self.children.append(playlist)
		return playlist
	
	def playlistByID(self,id):
		for child in self.children:
			if child.id()==id:
				return child
		return None
	
	def deletePlaylist(self,index):
		try: del self.children[index]
		except: pass

	def deletePlaylistByID(self,id):
		try: self.children.remove(self.playlistByID(id))
		except: pass
	
	def defaultPlaylist(self):
		try: return playlist(0)
		except:
			playlist = self.addPlaylist()
			playlist.hidden = 1
			title = playlist.addString(TITLE)
			title.setString('iPod')
			return playlist
	
	def clearPlaylists(self,createDefault = False):
		self.children = []
		if createDefault:
			self.defaultPlaylist()

	def masterPlaylist(self):
		for child in self.children:
			if child.hidden: return child
		return None
		
	def prepareToWrite(self,songs):
		pl = self.masterPlaylist()
		if pl: pl.prepareToWrite(songs)
		else:
			print "mhlp_atom.prepareToWrite: can't find master playlist"

	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhlp'
		p['children'] = [child.getPList() for child in self.children]
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.children = []
		for item in p['children']:
			atom = self.atomForTag(item['tag'],self._version)
			if atom:
				atom = atom.setPList(item)
				self.children.append(atom)
		return self

#
# py = Playlist atom
#
class mhyp_atom(mh_atom):
	def __init__(self,version,master = False):
		mh_atom.__init__(self,version)
		self.mhodNum = 0
		self.hidden = master
		self.timeStamp = 0
		self.playlistIDLo = 0
		self.playlistIDHi = 0
		self.stringMhodCount = 1 # count of mhods.type<50
		self.libraryMhodCount = 0 # count of mhods.type==52
		self.sortOrder = 0
		if master: self.sortOrder = 4
		self.mhip_children = []
		self.mhod_children = []
		self.buildDefaultChildren(version)
	
	def buildDefaultChildren(self,version):
		if self.hidden:
			self.mhod_children.append(mhod_atom_LibraryIndex(version,3))
			self.mhod_children.append(mhod_atom_LibraryIndex(version,4))
			self.mhod_children.append(mhod_atom_LibraryIndex(version,5))
			self.mhod_children.append(mhod_atom_LibraryIndex(version,7))
			self.mhod_children.append(mhod_atom_LibraryIndex(version,18))
			

	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		mhodCount = self.getul(f)
		mhipCount = self.getul(f)
		self.hidden = self.getul(f) # 1= hidden
		self.timeStamp = self.getul(f)
		self.playlistIDLo = self.getul(f)
		self.playlistIDHi = self.getul(f)
		self.stringMhodCount = self.getul(f) # should be 1
		self.libraryMhodCount = self.getul(f)  # should be 0
		self.sortOrder= self.getul(f) # 4==library, 5==smart playlist 0x18==podcast playlist
		self.seek(f,h1)
		#print "mhyp: got",mhodCount,"mhods"
		#print "mhyp: got",mhipCount,"mhips"
		self.mhod_children = []
		for i in xrange(mhodCount):
			#print i,
			self.mhod_children.append(self.atom(f))
		self.mhip_children = []
		for i in xrange(mhipCount):
			tag = self.get4cc(f)
			a = self.parseAtom(tag,f,version)
			self.mhip_children.append(a)
			mark = f.tell() # skip additional mhod if present
			if self.get4cc(f)=='mhod':
				self.parseAtom(tag,f,version)
			else:
				f.seek(mark)
		return self

	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhyp',0x6c)
		self.putul(f,len(self.mhod_children))
		self.putul(f,len(self.mhip_children))
		self.putul(f,self.hidden)
		self.putul(f,self.timeStamp)
		self.putul(f,self.playlistIDLo)
		self.putul(f,self.playlistIDHi)
		self.putul(f,self.stringMhodCount)
		self.putul(f,self.libraryMhodCount)
		self.putul(f,self.sortOrder)
		self.pad(f,mark,0x6c)
		self.validateHeader(f,mark,0x6c)
		for child in self.mhod_children:
			child.write(f,version)
		#i = 1
		for child in self.mhip_children:
			child.write(f,version)
			if version<mh_atom.VERSION4_9:
				self.writeFakeMhod(f,child.unk2)
			#i = i+1
		self.backpatch(f,mark)
		
	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhyp'
		p['hidden'] = self.hidden
		p['timeStamp'] = self.timeStamp
		p['playlistIDLo'] = self.playlistIDLo
		p['playlistIDHi'] = self.playlistIDHi
		p['sortOrder'] = self.sortOrder
		p['mhod_children'] = [child.getPList() for child in self.mhod_children]
		p['mhip_children'] = [child.getPList() for child in self.mhip_children]
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.hidden = p['hidden']
		self.timeStamp = p['timeStamp']
		self.playlistIDLo = p['playlistIDLo']
		self.playlistIDHi = p['playlistIDHi']
		self.sortOrder = p['sortOrder']
		self.mhod_children = []
		for item in p['mhod_children']:
			atom = self.atomForTag(item['tag'],self._version)
			if atom:
				atom = atom.setPList(item)
				self.mhod_children.append(atom)
		self.mhip_children = []
		for item in p['mhip_children']:
			atom = self.atomForTag(item['tag'],self._version)
			if atom:
				atom = atom.setPList(item)
				self.mhip_children.append(atom)
		return self

	def writeFakeMhod(self,f,i):
		#print "writing fake mhod"
		self.put4cc(f,'mhod')
		self.putul(f,0x18)
		self.putul(f,0x2c)
		self.putul(f,100) # type
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,i)
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,0)
	
	def tracks(self):
		return self.mhip_children
	
	def removeTrackID(self,id):
		for mhip in self.mhip_children:
			if mhip.trackID==id:
				self.mhip_children.remove(mhip)
				return
	
	def addTrackID(self,id=0):
		mhip = mhip_atom(self._version,id)
		self.mhip_children.append(mhip)
		#print "mhyp.addTrackID: now have",len(self.mhip_children)
		return mhip
	
	def name(self):
		for mhod in self.mhod_children:
			if mhod.type==mh_atom.TITLE: return mhod.string
		return "Playlist %d" % self.id()
	
	def setName(self,name):
		for mhod in self.mhod_children:
			if mhod.type==mh_atom.TITLE: 
				mhod.setString(name)
				return
		mhod = mhod_atom_String(self._version) # not found, add one
		mhod.setString(name)
		mhod.type = mh_atom.TITLE
		self.mhod_children.append(mhod)
	
	
	def id(self):
		return (self.playlistIDHi<<32)+self.playlistIDLo
		
	def setID(self,id):
		self.playlistIDHi = id>>32
		self.playlistIDLo = id & (1L<<32)

	def prepareToWrite(self,songs):
		#print "mhyp.prepareToWrite: building"
		for child in self.mhod_children:
			child.prepareToWrite(songs)

#
# pi = PlaylistItem atom
#
class mhip_atom(mh_atom):
	def __init__(self,version,id = 0):
		mh_atom.__init__(self,version)
		self.dataObjectChildCount = 1
		self.podcastGroupingFlag = 0
		self.groupID = id+1
		self.trackID = id
		self.timeStamp = 0
		self.podcastGroupingReference = 0
		
	def read(self,f,version):
		(h1,h2) = self.simpleHeader(f)
		self.dataObjectChildCount = self.getul(f) # should be 1
		self.podcastGroupingFlag = self.getul(f) # 0=normal file, 0x100=podcast group
		self.groupID = self.getul(f) # unique ID for track
		self.trackID = self.getul(f)
		self.timeStamp = self.getul(f)
		self.podcastGroupingReference = self.getul(f) #0 for normal files, parent group ID for podcasts
		self.seek(f,h2)
		return self
	
	def write(self,f,version):
		mark = self.putSimpleHeader(f,'mhip',0x4c)
		self.putul(f,self.dataObjectChildCount)
		self.putul(f,self.podcastGroupingFlag)
		self.putul(f,self.groupID)
		self.putul(f,self.trackID)
		self.putul(f,self.timeStamp)
		self.putul(f,self.podcastGroupingReference)
		self.pad(f,mark,0x4c)
		self.validateHeader(f,mark,0x4c)
		if version>=mh_atom.VERSION4_9:
			self.writeFakeMhod(f,self.groupID)
		self.backpatch(f,mark)		

	def getPList(self):
		p = mh_atom.getPList(self)
		p['tag'] = 'mhip'
		p['dataObjectChildCount'] = self.dataObjectChildCount
		p['podcastGroupingFlag'] = self.podcastGroupingFlag
		p['groupID'] = self.groupID
		p['trackID'] = self.trackID
		p['timeStamp'] = self.timeStamp
		p['podcastGroupingReference'] = self.podcastGroupingReference
		return p

	def setPList(self,p):
		mh_atom.setPList(self,p)
		self.dataObjectChildCount = p['dataObjectChildCount']
		self.podcastGroupingFlag = p['podcastGroupingFlag']
		self.groupID = p['groupID']
		self.trackID = p['trackID']
		self.timeStamp = p['timeStamp']
		self.podcastGroupingReference = p['podcastGroupingReference']
		return self

	def writeFakeMhod(self,f,i):
		#print "writing fake mhod"
		self.put4cc(f,'mhod')
		self.putul(f,0x18)
		self.putul(f,0x2c)
		self.putul(f,100) # type
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,i)
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,0)
		self.putul(f,0)

if __name__=='__main__':
	#infile =  "/home/dmaxwell/iPod_Control/iTunes/iTunesDBx"
	infile = "/mnt/sda2/iPod_Control/iTunes/iTunesDB"
	outfile = "/home/dmaxwell/iPod_Control/iTunes/iTunesDB.out"
	print "reading"
	f = open(infile,"rb")
	p = mh_atom().atom(f)
	if False:
		f = FileWriteString()
		p.write(f,p.versionlo)
		s = f.asString()
	else:
		pl = p.getPList()
		from PListParser import PListWriter, PListReader
		s =  PListWriter().unparseToString([pl])
	print "writing"
	f = open(outfile,"w")
	f.write(s)
	f.close()
	print "done"
